home *** CD-ROM | disk | FTP | other *** search
/ Skunkware 98 / Skunkware 98.iso / osr5 / sco / scripts / admin / ftpreport < prev    next >
Encoding:
Korn shell script  |  1997-08-26  |  16.8 KB  |  570 lines

  1. #!/bin/ksh
  2. # @(#) ftpreport.ksh 1.0 97/02/25
  3. # 94/07/18 john h. dubois iii (john@armory.com)
  4. # 94/07/29 Added -s flag.
  5. # 94/07/30 Merged ftpreport & httpreport.  Do not follow symlinks.
  6. # 97/02/25 Added -m option.
  7. #
  8. # todo: merge with httpreport again.
  9. # todo: Let users specify what files they are interested in.
  10. # todo: Count multiple accesses by one person only once.
  11. # todo: Show last modify date on files.
  12.  
  13. # Usage: Ind <arrayname> <value> [[<nsearch>] <firstelem>]
  14. # Returns the index of the first element of <arrayname> that has value <value>.
  15. # Returns 0 if it is none found.
  16. # Works only for indexes 1..255.
  17. # If <nsearch> is given, the first <nsearch> elements of the array are
  18. # searched, with only nonempty elements counted.
  19. # If not, the first n nonempty elements are searched,
  20. # where n is the number of elements in the array.
  21. # If a fourth argument is given, it is the index to start with; the search
  22. # continues for <nsearch> elements.
  23. # Element zero should not be set.
  24. function Ind {
  25.     typeset -i NElem ElemNum=${4:-1} NumNonNull=0
  26.     typeset Arr=$1 Val=$2 ElemVal
  27.  
  28.     [ $# -eq 3 ] && NElem=$3 || eval NElem=\${#$Arr[*]}
  29.     while [ ElemNum -le 255 -a NumNonNull -lt NElem ]; do
  30.     eval ElemVal=\"\${$Arr[ElemNum]}\" 
  31.     [ "$Val" = "$ElemVal" ] && return $ElemNum
  32.     [ -n "$ElemVal" ] && let NumNonNull+=1
  33.     let ElemNum+=1
  34.     done
  35.     return 0
  36. }
  37.  
  38. # Associative array routines
  39. # 93/06/25 john h. dubois iii (john@armory.com)
  40. # 93/07/09 Changed syntax of AStore so that these functions can be used
  41. #          for set operations.
  42. # 94/06/26 Added append capability to AStore
  43. # These routines use two shell arrays and an integer variable for each
  44. # associative array:
  45. # For associative array "foo", the values are stored in foo_val[] and the
  46. # indices are stored in foo_ind[].  The free pointer is stored in foo_free.
  47. # Only 255 values can be stored.
  48. # Arrays must have names that are valid shell variable names.
  49. # A null array index is not allowed.
  50.  
  51. # Usage: AStore <arrayname> <index> <value> <append>
  52. # Stores value <value> in associative array <arrayname> with index <index>
  53. # If no <value> is given, nothing is stored in the value array.
  54. # This can be used for set operations.
  55. # If a 4th argument is given, the value is appended to the current value
  56. # stored for the index (if any).
  57. # Return value is 0 for success, 1 for failure due to full array,
  58. # 2 for failure due to bad index or arrayname, 3 for bad syntax
  59. function AStore {
  60.     typeset Arr=$1 Index=$2 Val=$3
  61.     typeset -i Used Free=0 NumInd NumArgs=$#
  62.  
  63.     [ -z "$Index" ] && return 2
  64.     if eval [ -z \"\$${Arr}_free\" ]; then    # New array
  65.     [[ "$Arr" != [a-zA-Z_]*([a-zA-Z_0-9]) ]] && return 2
  66.     Free=1
  67.     NumInd=0
  68.     else
  69.     Ind ${Arr}_ind "$Index"
  70.     NumInd=$?
  71.     fi
  72.     if [ NumInd -eq 0 ]; then    # Index is not used yet
  73.     if [ Free -eq 0 ]; then
  74.         eval Used=\${#$Arr[*]} Free=\$${Arr}_free
  75.         [ Used -eq 255 ] && return 1
  76.     fi
  77.     # Find an unused element
  78.     while eval [ -n \"\${${Arr}_ind[Free]}\" ]; do
  79.         let Free+=1
  80.         [ Free -gt 255 ] && Free=1
  81.     done
  82.     NumInd=Free
  83.     let Free+=1
  84.     eval ${Arr}_free=$Free ${Arr}_ind[NumInd]=\$Index
  85.     fi
  86.     case $NumArgs in
  87.     [12]) return 1;;
  88.     3) eval ${Arr}_val[NumInd]=\$Val;;
  89.     4) eval ${Arr}_val[NumInd]=\"\${${Arr}_val[NumInd]}\$Val\";;
  90.     *) return 3;;
  91.     esac
  92. }
  93.  
  94. # Usage: AGet <arrayname> <index> <var>
  95. # Finds the value indexed by <index> in associative array <arrayname>.
  96. # If there is no such array or index, 0 is returned and <var> is not touched.
  97. # Otherwise, <var> (if given) is set to the indexed value and the numeric index
  98. # for <index> in the arrays is returned.
  99. function AGet {
  100.     typeset Arr=$1 Index=$2 Var=$3
  101.     typeset -i NumInd
  102.  
  103.     Ind ${Arr}_ind "$Index"
  104.     NumInd=$?
  105.     [ NumInd -gt 0 ] && [ -n "$Var" ] && eval $Var=\"\${${Arr}_val[NumInd]}\"
  106.     return $NumInd
  107. }
  108.  
  109. # Usage: AUnset <arrayname>
  110. # Removes all elements from associative array <arrayname>
  111. function AUnset {
  112.     typeset Arr=$1
  113.     eval unset ${Arr}_ind[*] ${Arr}_val[*] ${Arr}_free
  114. }
  115.  
  116. # Usage: ADelete <arrayname> <index>
  117. # Removes index <index> from associative array <arrayname>
  118. # Returns 0 on success, 1 if <index> was not an index of <arrayname>
  119. function ADelete {
  120.     typeset Arr=$1 Index=$2
  121.     typeset -i NumInd
  122.  
  123.     Ind ${Arr}_ind "$Index"
  124.     NumInd=$?
  125.     if [ NumInd -gt 0 ]; then
  126.     eval unset ${Arr}_ind[NumInd] ${Arr}_val[NumInd]
  127.     eval [ NumInd -lt ${Arr}_free ] && eval ${Arr}_free=$NumInd
  128.     return 0
  129.     else
  130.     return 1
  131.     fi
  132. }
  133.  
  134. # Usage: AGetAll <arrayname> <varname>
  135. # All of the indices of array <arrayname> are stored in shell array <varname>
  136. # with indices starting with 0.
  137. # The total number of indices is returned.
  138. function AGetAll {
  139.     typeset -i NElem ElemNum=1 NumNonNull=0
  140.     typeset Arr=$1 VarName=$2 ElemVal
  141.  
  142.     eval NElem=\${#${Arr}_ind[*]}
  143.     while [ ElemNum -le 255 -a NumNonNull -lt NElem ]; do
  144.     eval ElemVal=\"\${${Arr}_ind[ElemNum]}\" 
  145.     if [ -n "$ElemVal" ]; then
  146.         eval $VarName[NumNonNull]=\$ElemVal
  147.         let NumNonNull+=1
  148.     fi
  149.     let ElemNum+=1
  150.     done
  151.     return $NumNonNull
  152. }
  153.  
  154. # Read a defaults file
  155. # Usage: ReadDefaults filename var ...
  156. # Any of the named vars that are listed in the file are set globally
  157. function ReadDefaults {
  158.     typeset Defaults var file=$1
  159.     shift
  160.  
  161.     set_Avars Defaults "$file"
  162.     for var in "$@"; do
  163.     AGet Defaults $var $var
  164.     done
  165. }
  166.  
  167. # set_Avars: store variable assignments in an associative array.
  168. # 93/12/28 John H. DuBois III (john@armory.com)
  169. # Converts values to forms that won't be messed with by the shell.
  170. # Usage: set_Avars [-c] array-name [filename ...]
  171. # where the lines in filename (or the input) are of the form
  172. # var=value
  173. # value may contain spaces, backslashes, quote characters, etc.;
  174. # they will become part of the value assigned to index var.
  175. # Lines that begin with a # (optionally preceded by whitespace)
  176. # and lines that do not contain a '=' are ignored.
  177. # Variables are stored in associative array array-name.
  178. # If -c is given, an error message is printed & the program is exited
  179. # if an attempt is made to set a value for a parameter that has already
  180. # been set.
  181.  
  182. function set_Avars {
  183.     typeset Arr store
  184.  
  185.     if [ "$1" = -c ]; then
  186.     store=ChkStore
  187.     shift
  188.     else
  189.     store=AStore
  190.     fi
  191.     Arr=$1
  192.     shift
  193.     for file; do
  194.     if [ ! -r "$file" ]; then
  195.         print -u2 "$file: Could not open."
  196.         return 1
  197.     fi
  198.     done
  199.     # return exit status of eval
  200.     eval "$(sed "
  201. /^[     ]*#/d
  202. /=/!d
  203. s/'/'\\\\''/g
  204. s/=/ '/
  205. s/$/'/
  206. s/^/$store $Arr /" "$@")"
  207. }
  208.  
  209. # Usage: ChkStore <arrname> <index> <value>
  210. # Exit if <index> is already set
  211. function ChkStore {
  212.     typeset arrname=$1 index=$2 value=$3
  213.  
  214.     if AGet $arrname $index; then
  215.     # 0 return means index not found
  216.     AStore $arrname $index "$value"
  217.     else
  218.     print -u2 "Error: $index already set.  Exiting."
  219.     exit 1
  220.     fi
  221. }
  222.  
  223. # start of main program
  224.  
  225. name=${0##*/}
  226. Usage="Usage:
  227. $name [-ahtx] [-d DefReportLevel] [-c ConfFile] [-s Subject] [Logfile ...]"
  228. typeset -i IsFTP=0
  229.  
  230. case $name in
  231. httpreport)
  232.     DefLogFile=/usr/local/lib/httpd/logs/access_log
  233.     ;;
  234. ftpreport)
  235.     DefLogFile=/usr/adm/xferlog
  236.     AnonOnly=true   # Whether only rept on anonymous ftp use (optimization)
  237.     ftphome=$(print ~ftp)
  238.     IsFTP=1
  239.     ;;
  240. *)
  241.     print -u2 "Unknown function: $name"
  242.     exit 1
  243.     ;;
  244. esac
  245.  
  246. Type=${name%report}
  247. typeset -u Upper=$Type
  248. Config=/etc/default/$name
  249. UserConfig=.$name
  250. LogProc=${Type}log
  251. Subject="$Upper Report"
  252. DefReportLevel=none
  253. Archive=false
  254. Debug=false
  255. Test=false
  256. TooManyUsers=false
  257. mail=mail
  258.  
  259. while getopts :ahxtd:c:s:m: opt; do
  260.     case $opt in
  261.     h)
  262.     echo \
  263. "$name: mail reports of $Type accesses to users.
  264. $Usage
  265. $name examines the $Type access log and generates reports based on it,
  266. which are mailed to the users responsible for particular ${Type}-accessible
  267. files.  The default is for no report to be sent.  To get a report, a user
  268. can put the following in a file named $UserConfig in the user's home directory:
  269. raw        The raw $Type access log data is delivered, with one line for each
  270.        access.
  271. standard   A summary of accesses with one line for each file that has been
  272.        accessed one or more times.
  273. standard-wide  This gives the standard report without lines being truncated,
  274.        as is usually done to make the lines fit an 80-column screen."
  275. ((IsFTP)) && echo \
  276. "detailed   Some fields are shortened to allow more fields to be fit.
  277. detailed-wide  This gives the detailed report without lines being truncated,
  278.        as is usually done to make the lines fit an 80-column screen."
  279.     echo \
  280. "Multiple report levels may be specified, spread over one or more lines.  All
  281. of the requested reports will be sent.
  282. The operation of $name as a whole is determined by the
  283. $DefLogFile file.  This file must exist.  The format is
  284. Pattern    User
  285. where Pattern is a ksh-style expression to match the full pathname of a
  286. file that may be accessed.  It is used as a pattern rather than expanded as
  287. a filename, so * may match more than one filename component.  User is the user
  288. that reports of accesses that match this pattern should be sent to."
  289. ((IsFTP)) && echo \
  290. "A leading ~/ is expanded into the anonymous ftp user home directory, and a
  291. leading ~user is expanded into user's home directory."
  292. echo \
  293. "As a special case, a line that contains only one field can be used to match
  294. files in users' directories.  The string %d is embedded in the path where the
  295. user name will appear.  Lines that begin with # are comments and are ignored.
  296. Reports can be generated for no more than 255 users.
  297. Example config file:
  298. ######"
  299. ((IsFTP)) && echo \
  300. "# report transfers of files below each directory pub/user/foo to foo
  301. ~ftp/pub/user/%d/*
  302. # Report transfers of files below this dir to jon
  303. ~ftp/pub/midnight_beach/*       jon
  304. # report transfers of files in pub (but not in a dir below it) to spcecdt
  305. ~ftp/pub/*([!/])        spcecdt" || echo \
  306. "# report accesses of objects in users' home pages to the users
  307. /~%d/*
  308. # Report cgi-bin accesses to spcecdt
  309. /cgi-bin/*    spcecdt
  310. # report all accesses other than user home pages to webmaster
  311. /[!~]*    webmaster" 
  312. echo \
  313. "######
  314. Options:
  315. -a: After processing the $Type log file, append its contents to a file with
  316.     the same name but with - appended, and then truncate the log file.
  317. -h: Print this help.
  318. -s<subject>: Set the subject used in mail to users.  The default is '$Subject'.
  319. -x: Turn on debugging.
  320. -t: Test only; do not actually mail anything.
  321. -m<mailer>: Use <mailer> to send mail.  It must have a -s option to set the
  322.    subject of the mail.
  323. -d<default-report-level>: Set the default report level to something other than 
  324.    none.  More than one word can be given by quoting them.
  325. -c<config-file>: Use <config-file> instead of $Config."
  326.     exit 0
  327.     ;;
  328.     a)
  329.     Archive=true;;
  330.     x)
  331.     Debug=true
  332.     print -u2 "Debugging is on."
  333.     ;;
  334.     t)
  335.     Test=true
  336.     print -u2 "Test-only is on."
  337.     ;;
  338.     m)
  339.     mail=$OPTARG
  340.     ;;
  341.     s)
  342.     Subject=$OPTARG
  343.     ;;
  344.     d)
  345.     DefReportLevel=$OPTARG
  346.     ;;
  347.     c)
  348.     Config=$OPTARG
  349.     ;;
  350.     +?)
  351.     print -u2 "$name: options should not be preceded by a '+'."
  352.     exit 1
  353.     ;;
  354.     :)
  355.         print -r -u2 -- \
  356.         "$name: Option '$OPTARG' requires a value.  Use -h for help."
  357.         exit 1
  358.         ;;
  359.     ?) 
  360.     print -u2 "$name: $OPTARG: bad option.  Use -h for help."
  361.     exit 1
  362.     ;;
  363.     esac
  364. done
  365.  
  366. # remove args that were options
  367. let OPTIND=OPTIND-1
  368. shift $OPTIND
  369.  
  370. [ $# -lt 1 ] && set -- $DefLogFile
  371.  
  372. # Add the directory that this command was found in (if included in $0)
  373. # since log processing program may be there too, and it may not be in PATH
  374. PATH=$PATH:/usr/local/bin:${0%/*}
  375.  
  376. $Debug && print -u2 "Archiving=$Archive; test=$Test;
  377. report level=$DefReportLevel; config file=$Config; log files=$*"
  378.  
  379. if [ ! -f $Config ]; then
  380.     print -u2 "$name: Could not open config file $Config.  Aborting."
  381.     exit 1
  382. fi
  383.  
  384. # Store patterns
  385. typeset -i NumPat=0
  386. while read pat user; do
  387.     $Debug && print -u2 "Config line: $pat $user"
  388.     [[ "$pat" = \#* ]] && continue
  389.     if ((IsFTP)); then
  390.     if [[ "$pat" = \~* ]]; then    # expand ~
  391.         userhome=$(eval print ${pat%%/*})
  392.         if [[ $userhome = ~* ]]; then
  393.         print -u2 "$name: ${userhome#~}: no such user.  Aborting."
  394.         exit 1
  395.         fi
  396.         pat=$userhome/${pat#*/}
  397.     fi
  398.     [[ $pat != $ftphome?(/*) ]] && AnonOnly=false
  399.     fi
  400.  
  401.     # Convert %d line to more easily used format
  402.     $Debug && print -u2 "Pattern: $pat    User: $user"
  403.     if [ -z "$user" ]; then
  404.     if [[ "$pat" != *%d* ]]; then
  405.         print -u2 \
  406. "$name: Error in $Config:
  407. this line has only one field, which does not contain %d:
  408. $pat $user"
  409.         exit 1
  410.     fi
  411.     Prefixes[NumPat]=${pat%%%d*}
  412.     Suffixes[NumPat]=${pat#*%d}
  413.     Pats[NumPat]=${Prefixes[NumPat]}*([!/])${Suffixes[NumPat]}
  414.     else
  415.     Pats[NumPat]=$pat
  416.     Users[NumPat]=$user
  417.     fi
  418.     let NumPat+=1
  419. done < $Config
  420.  
  421. # Pats[1..n] now contains ksh patterns to match each xferred file against.
  422. # If the username to mail to is given explicitly, it is in Users[n].
  423. # If it is part of the filename, then a prefix and suffix that should be
  424. # removed from the filename to yield the username are stored in Prefixes[n]
  425. # and Suffixes[n].
  426.  
  427. if $Debug; then
  428.     typeset -i i=0
  429.     while [ i -lt NumPat ]; do
  430.     print -n "Rule $i: Report on ${Pats[i]} to "
  431.     if [ -n "${Users[i]}" ]; then
  432.         print "${Users[i]}"
  433.     else
  434.         print "pattern - prefix ${Prefixes[i]} and suffix ${Suffixes[i]}"
  435.     fi
  436.     let i+=1
  437.     done
  438. fi
  439.  
  440. # Example lines from httpd log:
  441. #      1              2   3   4    5       6    7     8                 9
  442. # Requesting-host    Date                       Op   URL                Version
  443. # deeptht.armory.com [Sat Feb 19 17:53:27 1994] GET  /~spcecdt/arm.html HTTP/1.0
  444. # sgil301.cern.ch    [Mon Jul 18 07:35:42 1994] POST /cgi-bin/purity-test/NumQuest=100/Name=Sex100 HTTP/1.0
  445. # netcom5.netcom.com [Sun May 22 00:17:16 1994] get  /u/css1217/index.html
  446. # pentlan.stir.ac.uk [Wed May 25 01:17:29 1994] HEAD /~zap/nc/nc.html HTTP/1.0
  447. # Other ops are ignored.
  448.  
  449. # Example line from xferlog:
  450. # Fri Mar 25 14:29:11 1994 5 si.UCSC.EDU 33416 /u/prologic/toolbox6.1 a _ o r prologic ftp 0 *
  451. #current-time   transfer-time   remote-host   file-size   filename
  452. #    1-5                6               7       8               9
  453. #transfer-type   special-action-flag   direction   access-mode   username
  454. #       10              11              12              13              14
  455. #service-name   authentication-method   authenticated-user-id
  456. #       15              16                      17
  457.  
  458. set -o noglob
  459. typeset -i i NumUsers
  460.  
  461. # Process report files & construct report for each user
  462. for logfile in "$@"; do
  463.     while read line; do
  464.     set -- $line
  465.  
  466.     if ((IsFTP)); then
  467.         # If anonymous xfer (chrooted), prepend ftp home dir
  468.         [ "${13}" = a ] && filename="$ftphome$9" || filename=$9
  469.         # Optimization.  Do this separately from the above test because
  470.         # user logged in directly might grab from pub archives.
  471.         # This doesn't actually help that much.
  472.         $AnonOnly && [[ "$filename" != $ftphome?(/*) ]] && continue
  473.     else
  474.         filename=$8
  475.     fi
  476.  
  477.     $Debug && print -n -u2 .
  478.     let i=0
  479.     # Find which reports this line should be included in
  480.     while [ i -lt NumPat ]; do
  481.         if eval [[ \$filename = ${Pats[i]} ]]; then
  482.         if [ -n "${Users[i]}" ]; then
  483.             User=${Users[i]}
  484.         else
  485.             User=${filename##${Prefixes[i]}}
  486.             User=${User%%${Suffixes[i]}}
  487.         fi
  488.         # Append line to user's data
  489.         AStore XferData "$User" "$line
  490. " - || TooManyUsers=true
  491.         fi
  492.         let i+=1
  493.     done
  494.     done < $logfile
  495. done
  496.  
  497. $Debug && print -u2 ""
  498.  
  499. $TooManyUsers &&
  500. print -u2 "$name: Too many users; report limited to first 255."
  501.  
  502. # Data for each user is now stored in associative array XferData, indexed by
  503. # user name.
  504.  
  505. AGetAll XferData Indices
  506. NumUsers=$?
  507. typeset DataLines
  508.  
  509. $Debug && 
  510. print -u2 "Reports to be generated for $NumUsers users: ${Indices[*]}"
  511.  
  512. i=0
  513. while [ i -lt NumUsers ]; do
  514.     User=${Indices[i]}
  515.     eval UserConfFile=~$User/$UserConfig
  516.     $Debug && print -u2 "Checking for existance of $UserConfFile"
  517.     if [ -f $UserConfFile -a ! -L $UserConfFile ]; then
  518.     $Debug && print -u2 "$User has a config file."
  519.     Level=
  520.     while read line; do
  521.         [[ $line = [!#]* ]] && Level="$Level $line"
  522.         $Debug && print -u2 ">> $line"
  523.     done < $UserConfFile
  524.     else
  525.     Level=$DefReportLevel
  526.     fi
  527.     $Debug && print -u2 "Report level(s) for $User: $Level"
  528.     unset ReportCommands[*]
  529.     for word in $Level; do
  530.     case "$word" in
  531.     none)
  532.         ReportCmd=
  533.         ;;
  534.     raw)
  535.         ReportCmd=cat
  536.         ;;
  537.     standard|*detailed*|*wide*)
  538.         flags=
  539.             # Match embedded 'detailed' or 'wide' so that detailed-wide or
  540.             # wide-detailed can be given
  541.             ((IsFTP)) && [ "$word" = *detailed* ] && flags=-l
  542.             [ "$word" = *wide* ] && flags="$flags -w0"
  543.             ReportCmd="$LogProc $flags /dev/stdin"
  544.  
  545.         ;;
  546.     *)
  547.         ReportCmd="echo Bad value in $UserConfFile; should be some of
  548. none, raw, standard, or wide."
  549.         ;;
  550.     esac
  551.     set -A ReportCommands -- "${ReportCommands[@]}" "$ReportCmd"
  552.     done
  553.     AGet XferData $User DataLines
  554.     if $Debug; then
  555.     print -u2 "Report commands for $User: ${ReportCommands[*]}"
  556.     print -u2 "Data lines for $User:\n$DataLines"
  557.     fi
  558.     for Cmd in "${ReportCommands[@]}"; do
  559.     print -r "$DataLines" | $Cmd
  560.     echo ""
  561.     done | if $Test; then
  562.     print -u2 "Would send to $User:"
  563.     cat 1>&2
  564.     elif [ -n "$ReportCmd" ]; then
  565.     $mail -s "$Subject" $User
  566.     fi
  567.     let i+=1
  568. done
  569. exit 0
  570.